/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.core;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.lucene.search.IndexSearcher;
import org.apache.solr.api.ClusterPluginsSource;
import org.apache.solr.api.ContainerPluginsRegistry;
import org.apache.solr.api.NodeConfigClusterPluginsSource;
import org.apache.solr.client.solrj.impl.SolrZkClientTimeout;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.util.EnvUtils;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.logging.LogWatcherConfig;
import org.apache.solr.search.CacheConfig;
import org.apache.solr.servlet.SolrDispatchFilter;
import org.apache.solr.update.UpdateShardHandlerConfig;
import org.apache.solr.util.ModuleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NodeConfig {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  // all Path fields here are absolute and normalized.

  private final String nodeName;

  private final Path coreRootDirectory;
  private final String coresLocatorClass;
  private final String coreSorterClass;

  private final Path solrDataHome;

  private final Integer booleanQueryMaxClauseCount;

  private final Path configSetBaseDirectory;

  private final Set<Path> allowPaths;

  private final List<String> allowUrls;

  private final boolean hideStackTraces;

  private final String sharedLibDirectory;

  private final String modules;

  private final Set<String> hiddenSysProps;
  private final Predicate<String> hiddenSysPropPattern;

  private final PluginInfo shardHandlerFactoryConfig;
  private final UpdateShardHandlerConfig updateShardHandlerConfig;

  private final String configSetServiceClass;

  private final String coreAdminHandlerClass;

  private final Map<String, String> coreAdminHandlerActions;

  private final String collectionsAdminHandlerClass;

  private final String healthCheckHandlerClass;

  private final String infoHandlerClass;

  private final String configSetsHandlerClass;

  private final LogWatcherConfig logWatcherConfig;

  private final CloudConfig cloudConfig;

  private final Integer coreLoadThreads;

  private final int replayUpdatesThreads;

  private final int indexSearcherExecutorThreads;

  private final boolean useSchemaCache;

  private final PluginInfo[] backupRepositoryPlugins;

  private final MetricsConfig metricsConfig;

  private final Map<String, CacheConfig> cachesConfig;

  private final PluginInfo tracerConfig;

  private final PluginInfo[] clusterPlugins;

  private final String defaultZkHost;

  private NodeConfig(
      String nodeName,
      Path coreRootDirectory,
      String coresLocatorClass,
      String coreSorterClass,
      Path solrDataHome,
      Integer booleanQueryMaxClauseCount,
      Path configSetBaseDirectory,
      String sharedLibDirectory,
      PluginInfo shardHandlerFactoryConfig,
      UpdateShardHandlerConfig updateShardHandlerConfig,
      String coreAdminHandlerClass,
      Map<String, String> coreAdminHandlerActions,
      String collectionsAdminHandlerClass,
      String healthCheckHandlerClass,
      String infoHandlerClass,
      String configSetsHandlerClass,
      LogWatcherConfig logWatcherConfig,
      CloudConfig cloudConfig,
      Integer coreLoadThreads,
      int replayUpdatesThreads,
      int indexSearcherExecutorThreads,
      boolean useSchemaCache,
      Path solrHome,
      SolrResourceLoader loader,
      Properties solrProperties,
      PluginInfo[] backupRepositoryPlugins,
      MetricsConfig metricsConfig,
      Map<String, CacheConfig> cachesConfig,
      PluginInfo tracerConfig,
      PluginInfo[] clusterPlugins,
      String defaultZkHost,
      Set<Path> allowPaths,
      List<String> allowUrls,
      boolean hideStackTraces,
      String configSetServiceClass,
      String modules,
      Set<String> hiddenSysProps) {
    // all Path params here are absolute and normalized.
    this.nodeName = nodeName;
    this.coreRootDirectory = coreRootDirectory;
    this.coresLocatorClass = coresLocatorClass;
    this.coreSorterClass = coreSorterClass;
    this.solrDataHome = solrDataHome;
    this.booleanQueryMaxClauseCount = booleanQueryMaxClauseCount;
    this.configSetBaseDirectory = configSetBaseDirectory;
    this.sharedLibDirectory = sharedLibDirectory;
    this.shardHandlerFactoryConfig = shardHandlerFactoryConfig;
    this.updateShardHandlerConfig = updateShardHandlerConfig;
    this.coreAdminHandlerClass = coreAdminHandlerClass;
    this.coreAdminHandlerActions = coreAdminHandlerActions;
    this.collectionsAdminHandlerClass = collectionsAdminHandlerClass;
    this.healthCheckHandlerClass = healthCheckHandlerClass;
    this.infoHandlerClass = infoHandlerClass;
    this.configSetsHandlerClass = configSetsHandlerClass;
    this.logWatcherConfig = logWatcherConfig;
    this.cloudConfig = cloudConfig;
    this.coreLoadThreads = coreLoadThreads;
    this.replayUpdatesThreads = replayUpdatesThreads;
    this.indexSearcherExecutorThreads = indexSearcherExecutorThreads;
    this.useSchemaCache = useSchemaCache;
    this.solrHome = solrHome;
    this.loader = loader;
    this.solrProperties = solrProperties;
    this.backupRepositoryPlugins = backupRepositoryPlugins;
    this.metricsConfig = metricsConfig;
    this.cachesConfig = cachesConfig == null ? Collections.emptyMap() : cachesConfig;
    this.tracerConfig = tracerConfig;
    this.clusterPlugins = clusterPlugins;
    this.defaultZkHost = defaultZkHost;
    this.allowPaths = allowPaths;
    this.allowUrls = allowUrls;
    this.hideStackTraces = hideStackTraces;
    this.configSetServiceClass = configSetServiceClass;
    this.modules = modules;
    this.hiddenSysProps = hiddenSysProps;
    this.hiddenSysPropPattern =
        Pattern.compile("^(" + String.join("|", hiddenSysProps) + ")$", Pattern.CASE_INSENSITIVE)
            .asMatchPredicate();

    if (this.cloudConfig != null && this.getCoreLoadThreadCount(false) < 2) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR,
          "SolrCloud requires a value of at least 2 for coreLoadThreads (configured value = "
              + this.coreLoadThreads
              + ")");
    }
    if (null == this.solrHome) throw new NullPointerException("solrHome");
    if (null == this.loader) throw new NullPointerException("loader");

    if (this.clusterPlugins != null
        && this.clusterPlugins.length > 0
        && !ClusterPluginsSource.resolveClassName()
            .equals(NodeConfigClusterPluginsSource.class.getName())) {
      throw new SolrException(
          ErrorCode.SERVER_ERROR,
          "Cluster plugins found in solr.xml but the property "
              + ContainerPluginsRegistry.CLUSTER_PLUGIN_EDIT_ENABLED
              + " is set to true. Cluster plugins may only be declared in solr.xml with immutable configs.");
    }

    setupSharedLib();
    initModules();
  }

  /**
   * Get the NodeConfig. This may also be used by custom filters to load relevant configuration.
   *
   * @return the NodeConfig
   */
  public static NodeConfig loadNodeConfig(Path solrHome, Properties nodeProperties) {
    final SolrResourceLoader loader = new SolrResourceLoader(solrHome);
    initModules(loader, null);
    nodeProperties = SolrXmlConfig.wrapAndSetZkHostFromSysPropIfNeeded(nodeProperties);

    // TODO: Only job of this block is to
    //  delay starting a solr core to satisfy
    //  ZkFailoverTest test case...
    String zkHost = nodeProperties.getProperty(SolrXmlConfig.ZK_HOST);
    if (StrUtils.isNotNullOrEmpty(zkHost)) {
      int startUpZkTimeOut =
          1000
              * Integer.getInteger(
                  "solr.cloud.wait.for.zk.seconds", SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT);

      try (SolrZkClient zkClient =
          new SolrZkClient.Builder()
              .withUrl(zkHost)
              .withTimeout(startUpZkTimeOut, TimeUnit.MILLISECONDS)
              .withConnTimeOut(startUpZkTimeOut, TimeUnit.MILLISECONDS)
              .withSolrClassLoader(loader)
              .build()) {
        zkClient.exists("/configs", true);
      } catch (Exception e) {
        throw new SolrException(
            ErrorCode.SERVER_ERROR, "Error occurred while testing zookeeper connection", e);
      }
    }

    return SolrXmlConfig.fromSolrHome(solrHome, nodeProperties);
  }

  public String getConfigSetServiceClass() {
    return this.configSetServiceClass;
  }

  public String getNodeName() {
    return nodeName;
  }

  /** Absolute. */
  public Path getCoreRootDirectory() {
    return coreRootDirectory;
  }

  public String getCoresLocatorClass() {
    return this.coresLocatorClass;
  }

  public String getCoreSorterClass() {
    return coreSorterClass;
  }

  /** Absolute. */
  public Path getSolrDataHome() {
    return solrDataHome;
  }

  /**
   * Obtain the path of solr's binary installation directory, e.g. <code>/opt/solr</code>
   *
   * @return path to install dir or null if solr.install.dir not set.
   */
  public static Path getSolrInstallDir() {
    String prop = System.getProperty(SolrDispatchFilter.SOLR_INSTALL_DIR_ATTRIBUTE);
    if (prop == null || prop.isBlank()) {
      log.debug("solr.install.dir property not initialized.");
      return null;
    }
    return Path.of(prop);
  }

  /**
   * If null, the lucene default will not be overridden
   *
   * @see IndexSearcher#setMaxClauseCount
   */
  public Integer getBooleanQueryMaxClauseCount() {
    return booleanQueryMaxClauseCount;
  }

  public PluginInfo getShardHandlerFactoryPluginInfo() {
    return shardHandlerFactoryConfig;
  }

  public UpdateShardHandlerConfig getUpdateShardHandlerConfig() {
    return updateShardHandlerConfig;
  }

  public int getCoreLoadThreadCount(boolean zkAware) {
    return coreLoadThreads == null
        ? (zkAware
            ? NodeConfigBuilder.DEFAULT_CORE_LOAD_THREADS_IN_CLOUD
            : NodeConfigBuilder.DEFAULT_CORE_LOAD_THREADS)
        : coreLoadThreads;
  }

  public int getReplayUpdatesThreads() {
    return replayUpdatesThreads;
  }

  public int getIndexSearcherExecutorThreads() {
    return indexSearcherExecutorThreads;
  }

  /**
   * Returns a directory, optionally a comma separated list of directories that will be added to
   * Solr's class path for searching for classes and plugins. The path is either absolute or
   * relative to SOLR_HOME. Note that SOLR_HOME/lib will always be added to the search path even if
   * not included in this list.
   *
   * @return a comma separated list of path strings or null if no paths defined
   */
  public String getSharedLibDirectory() {
    return sharedLibDirectory;
  }

  public String getCoreAdminHandlerClass() {
    return coreAdminHandlerClass;
  }

  public Map<String, String> getCoreAdminHandlerActions() {
    return coreAdminHandlerActions;
  }

  public String getCollectionsHandlerClass() {
    return collectionsAdminHandlerClass;
  }

  public String getHealthCheckHandlerClass() {
    return healthCheckHandlerClass;
  }

  public String getInfoHandlerClass() {
    return infoHandlerClass;
  }

  public String getConfigSetsHandlerClass() {
    return configSetsHandlerClass;
  }

  public boolean hasSchemaCache() {
    return useSchemaCache;
  }

  /** Absolute. */
  public Path getConfigSetBaseDirectory() {
    return configSetBaseDirectory;
  }

  public LogWatcherConfig getLogWatcherConfig() {
    return logWatcherConfig;
  }

  public CloudConfig getCloudConfig() {
    return cloudConfig;
  }

  protected final Path solrHome;
  protected final SolrResourceLoader loader;
  protected final Properties solrProperties;

  public Properties getSolrProperties() {
    return solrProperties;
  }

  public Path getSolrHome() {
    return solrHome;
  }

  public SolrResourceLoader getSolrResourceLoader() {
    return loader;
  }

  public PluginInfo[] getBackupRepositoryPlugins() {
    return backupRepositoryPlugins;
  }

  public MetricsConfig getMetricsConfig() {
    return metricsConfig;
  }

  public Map<String, CacheConfig> getCachesConfig() {
    return cachesConfig;
  }

  public PluginInfo getTracerConfiguratorPluginInfo() {
    return tracerConfig;
  }

  public PluginInfo[] getClusterPlugins() {
    return clusterPlugins;
  }

  /**
   * Extra file paths that will be allowed for core creation, in addition to SOLR_HOME,
   * SOLR_DATA_HOME and coreRootDir
   */
  public Set<Path> getAllowPaths() {
    return allowPaths;
  }

  /** Allow-list of Solr nodes URLs. */
  public List<String> getAllowUrls() {
    return allowUrls;
  }

  public boolean hideStackTraces() {
    return hideStackTraces;
  }

  // Configures SOLR_HOME/lib to the shared class loader
  private void setupSharedLib() {
    // Always add $SOLR_HOME/lib to the shared resource loader
    Set<String> libDirs = new LinkedHashSet<>();
    libDirs.add("lib");

    Path solrInstallDir = getSolrInstallDir();
    if (solrInstallDir == null) {
      log.warn(
          "Unable to add $SOLR_HOME/lib for shared lib since {} was not set.",
          SolrDispatchFilter.SOLR_INSTALL_DIR_ATTRIBUTE);
    } else {
      // Always add $SOLR_TIP/lib to the shared resource loader
      libDirs.add(solrInstallDir.resolve("lib").toAbsolutePath().normalize().toString());
    }

    if (StrUtils.isNotBlank(getSharedLibDirectory())) {
      List<String> sharedLibs = Arrays.asList(getSharedLibDirectory().split("\\s*,\\s*"));
      libDirs.addAll(sharedLibs);
    }

    addFoldersToSharedLib(libDirs);
  }

  /**
   * Returns the modules as configured in solr.xml. Comma separated list. May be null if not defined
   */
  public String getModules() {
    return modules;
  }

  /** Returns whether a given system property is hidden */
  public boolean isSysPropHidden(String sysPropName) {
    return hiddenSysPropPattern.test(sysPropName);
  }

  public static final String REDACTED_SYS_PROP_VALUE = "--REDACTED--";

  /** Returns the system property value, or "--REDACTED--" if the system property is hidden */
  public String getRedactedSysPropValue(String sysPropName) {
    return hiddenSysPropPattern.test(sysPropName)
        ? REDACTED_SYS_PROP_VALUE
        : System.getProperty(sysPropName);
  }

  // Finds every jar in each folder and adds it to shardLib, then reloads Lucene SPI
  private void addFoldersToSharedLib(Set<String> libDirs) {
    boolean modified = false;
    // add the sharedLib to the shared resource loader before initializing cfg based plugins
    for (String libDir : libDirs) {
      Path libPath = getSolrHome().resolve(libDir);
      if (Files.exists(libPath)) {
        try {
          loader.addToClassLoader(SolrResourceLoader.getURLs(libPath));
          modified = true;
        } catch (IOException e) {
          throw new SolrException(ErrorCode.SERVER_ERROR, "Couldn't load libs: " + e, e);
        }
      }
    }
    if (modified) {
      loader.reloadLuceneSPI();
    }
  }

  // Adds modules to shared classpath
  private void initModules() {
    initModules(loader, getModules());
  }

  // can't we move this to ModuleUtils?
  public static void initModules(SolrResourceLoader loader, String modules) {
    var moduleNames = ModuleUtils.resolveModulesFromStringOrSyspropOrEnv(modules);
    boolean modified = false;

    Path solrInstallDir = getSolrInstallDir();
    if (solrInstallDir == null) {
      if (!moduleNames.isEmpty()) {
        throw new SolrException(
            ErrorCode.SERVER_ERROR,
            "Unable to setup modules "
                + moduleNames
                + " because "
                + SolrDispatchFilter.SOLR_INSTALL_DIR_ATTRIBUTE
                + " was not set.");
      }
      return;
    }
    for (String m : moduleNames) {
      if (!ModuleUtils.moduleExists(solrInstallDir, m)) {
        log.error(
            "No module with name {}, available modules are {}",
            m,
            ModuleUtils.listAvailableModules(solrInstallDir));
        // Fail-fast if user requests a non-existing module
        throw new SolrException(ErrorCode.SERVER_ERROR, "No module with name " + m);
      }
      Path moduleLibPath = ModuleUtils.getModuleLibPath(solrInstallDir, m);
      if (Files.exists(moduleLibPath)) {
        try {
          List<URL> urls = SolrResourceLoader.getURLs(moduleLibPath);
          loader.addToClassLoader(urls);
          if (log.isInfoEnabled()) {
            log.info("Added module {}. libPath={} with {} libs", m, moduleLibPath, urls.size());
          }
          if (log.isDebugEnabled()) {
            log.debug("Libs loaded from {}: {}", moduleLibPath, urls);
          }
          modified = true;
        } catch (IOException e) {
          throw new SolrException(
              ErrorCode.SERVER_ERROR, "Couldn't load libs for module " + m + ": " + e, e);
        }
      } else {
        throw new SolrException(
            ErrorCode.SERVER_ERROR, "Module lib folder " + moduleLibPath + " not found.");
      }
    }
    if (modified) {
      loader.reloadLuceneSPI();
    }
  }

  public static class NodeConfigBuilder {
    // all Path fields here are absolute and normalized.
    private SolrResourceLoader loader;
    private Path coreRootDirectory;
    private String coresLocatorClass = DEFAULT_CORESLOCATORCLASS;
    private String coreSorterClass = DEFAULT_CORESORTERCLASS;
    private Path solrDataHome;
    private Integer booleanQueryMaxClauseCount;
    private Path configSetBaseDirectory;
    private String sharedLibDirectory;
    private String modules;
    private String hiddenSysProps;
    private PluginInfo shardHandlerFactoryConfig;
    private UpdateShardHandlerConfig updateShardHandlerConfig = UpdateShardHandlerConfig.DEFAULT;
    private String configSetServiceClass;
    private String coreAdminHandlerClass = DEFAULT_ADMINHANDLERCLASS;
    private Map<String, String> coreAdminHandlerActions = Collections.emptyMap();
    private String collectionsAdminHandlerClass = DEFAULT_COLLECTIONSHANDLERCLASS;
    private String healthCheckHandlerClass = DEFAULT_HEALTHCHECKHANDLERCLASS;
    private String infoHandlerClass = DEFAULT_INFOHANDLERCLASS;
    private String configSetsHandlerClass = DEFAULT_CONFIGSETSHANDLERCLASS;
    private LogWatcherConfig logWatcherConfig = new LogWatcherConfig(true, null, null, 50);
    private CloudConfig cloudConfig;
    private int coreLoadThreads = DEFAULT_CORE_LOAD_THREADS;
    private int replayUpdatesThreads = Runtime.getRuntime().availableProcessors();
    private int indexSearcherExecutorThreads = DEFAULT_INDEX_SEARCHER_EXECUTOR_THREADS;
    private boolean useSchemaCache = false;
    private Properties solrProperties = new Properties();
    private PluginInfo[] backupRepositoryPlugins;
    private MetricsConfig metricsConfig;
    private Map<String, CacheConfig> cachesConfig;
    private PluginInfo tracerConfig;
    private PluginInfo[] clusterPlugins;
    private String defaultZkHost;
    private Set<Path> allowPaths = Collections.emptySet();
    private List<String> allowUrls = Collections.emptyList();
    private boolean hideStackTrace =
        !EnvUtils.getPropertyAsBool("solr.responses.stacktrace.enabled", true);

    private final Path solrHome;
    private final String nodeName;

    public static final int DEFAULT_CORE_LOAD_THREADS = 3;
    // No:of core load threads in cloud mode is set to a default of 8
    public static final int DEFAULT_CORE_LOAD_THREADS_IN_CLOUD = 8;

    public static final int DEFAULT_INDEX_SEARCHER_EXECUTOR_THREADS = 0;

    private static final String DEFAULT_CORESLOCATORCLASS =
        "org.apache.solr.core.CorePropertiesLocator";
    private static final String DEFAULT_CORESORTERCLASS = "org.apache.solr.core.CoreSorter";
    private static final String DEFAULT_ADMINHANDLERCLASS =
        "org.apache.solr.handler.admin.CoreAdminHandler";
    private static final String DEFAULT_INFOHANDLERCLASS =
        "org.apache.solr.handler.admin.InfoHandler";
    private static final String DEFAULT_COLLECTIONSHANDLERCLASS =
        "org.apache.solr.handler.admin.CollectionsHandler";
    private static final String DEFAULT_HEALTHCHECKHANDLERCLASS =
        "org.apache.solr.handler.admin.HealthCheckHandler";
    private static final String DEFAULT_CONFIGSETSHANDLERCLASS =
        "org.apache.solr.handler.admin.ConfigSetsHandler";

    public static final Set<String> DEFAULT_HIDDEN_SYS_PROPS =
        Set.of(
            "javax\\.net\\.ssl\\.keyStorePassword",
            "javax\\.net\\.ssl\\.trustStorePassword",
            ".*credentials",
            "zkDigestPassword",
            "zkDigestReadonlyPassword",
            "aws\\.secretKey", // AWS SDK v1
            "aws\\.secretAccessKey", // AWS SDK v2
            "http\\.proxyPassword",
            ".*password.*",
            ".*secret.*");

    public NodeConfigBuilder(String nodeName, Path solrHome) {
      this.nodeName = nodeName;
      this.solrHome = solrHome.toAbsolutePath();
      this.coreRootDirectory = solrHome;
      // always init from sysprop because <solrDataHome> config element may be missing
      setSolrDataHome(System.getProperty(SolrXmlConfig.SOLR_DATA_HOME));
      setConfigSetBaseDirectory("configsets");
      this.metricsConfig = new MetricsConfig.MetricsConfigBuilder().build();
    }

    public NodeConfigBuilder setCoreRootDirectory(String coreRootDirectory) {
      this.coreRootDirectory = solrHome.resolve(coreRootDirectory).normalize();
      return this;
    }

    public NodeConfigBuilder setCoresLocatorClass(String coresLocatorClass) {
      this.coresLocatorClass = coresLocatorClass;
      return this;
    }

    public NodeConfigBuilder setCoreSorterClass(String coreSorterClass) {
      this.coreSorterClass = coreSorterClass;
      return this;
    }

    public NodeConfigBuilder setSolrDataHome(String solrDataHomeString) {
      // keep it null unless explicitly set to non-empty value
      if (solrDataHomeString != null && !solrDataHomeString.isEmpty()) {
        this.solrDataHome = solrHome.resolve(solrDataHomeString).normalize();
      }
      return this;
    }

    public NodeConfigBuilder setBooleanQueryMaxClauseCount(Integer booleanQueryMaxClauseCount) {
      this.booleanQueryMaxClauseCount = booleanQueryMaxClauseCount;
      return this;
    }

    public NodeConfigBuilder setConfigSetBaseDirectory(String configSetBaseDirectory) {
      this.configSetBaseDirectory = solrHome.resolve(configSetBaseDirectory);
      return this;
    }

    public NodeConfigBuilder setSharedLibDirectory(String sharedLibDirectory) {
      this.sharedLibDirectory = sharedLibDirectory;
      return this;
    }

    public NodeConfigBuilder setShardHandlerFactoryConfig(PluginInfo shardHandlerFactoryConfig) {
      this.shardHandlerFactoryConfig = shardHandlerFactoryConfig;
      return this;
    }

    public NodeConfigBuilder setUpdateShardHandlerConfig(
        UpdateShardHandlerConfig updateShardHandlerConfig) {
      this.updateShardHandlerConfig = updateShardHandlerConfig;
      return this;
    }

    public NodeConfigBuilder setCoreAdminHandlerClass(String coreAdminHandlerClass) {
      this.coreAdminHandlerClass = coreAdminHandlerClass;
      return this;
    }

    public NodeConfigBuilder setCoreAdminHandlerActions(
        Map<String, String> coreAdminHandlerActions) {
      this.coreAdminHandlerActions = coreAdminHandlerActions;
      return this;
    }

    public NodeConfigBuilder setCollectionsAdminHandlerClass(String collectionsAdminHandlerClass) {
      this.collectionsAdminHandlerClass = collectionsAdminHandlerClass;
      return this;
    }

    public NodeConfigBuilder setHealthCheckHandlerClass(String healthCheckHandlerClass) {
      this.healthCheckHandlerClass = healthCheckHandlerClass;
      return this;
    }

    public NodeConfigBuilder setInfoHandlerClass(String infoHandlerClass) {
      this.infoHandlerClass = infoHandlerClass;
      return this;
    }

    public NodeConfigBuilder setConfigSetsHandlerClass(String configSetsHandlerClass) {
      this.configSetsHandlerClass = configSetsHandlerClass;
      return this;
    }

    public NodeConfigBuilder setLogWatcherConfig(LogWatcherConfig logWatcherConfig) {
      this.logWatcherConfig = logWatcherConfig;
      return this;
    }

    public NodeConfigBuilder setCloudConfig(CloudConfig cloudConfig) {
      this.cloudConfig = cloudConfig;
      return this;
    }

    public NodeConfigBuilder setCoreLoadThreads(int coreLoadThreads) {
      this.coreLoadThreads = coreLoadThreads;
      return this;
    }

    public NodeConfigBuilder setReplayUpdatesThreads(int replayUpdatesThreads) {
      this.replayUpdatesThreads = replayUpdatesThreads;
      return this;
    }

    public NodeConfigBuilder setIndexSearcherExecutorThreads(int indexSearcherExecutorThreads) {
      this.indexSearcherExecutorThreads = indexSearcherExecutorThreads;
      return this;
    }

    public NodeConfigBuilder setUseSchemaCache(boolean useSchemaCache) {
      this.useSchemaCache = useSchemaCache;
      return this;
    }

    public NodeConfigBuilder setSolrProperties(Properties solrProperties) {
      this.solrProperties = solrProperties;
      return this;
    }

    public NodeConfigBuilder setBackupRepositoryPlugins(PluginInfo[] backupRepositoryPlugins) {
      this.backupRepositoryPlugins = backupRepositoryPlugins;
      return this;
    }

    public NodeConfigBuilder setMetricsConfig(MetricsConfig metricsConfig) {
      this.metricsConfig = metricsConfig;
      return this;
    }

    public NodeConfigBuilder setCachesConfig(Map<String, CacheConfig> cachesConfig) {
      this.cachesConfig = cachesConfig;
      return this;
    }

    public NodeConfigBuilder setTracerConfig(PluginInfo tracerConfig) {
      this.tracerConfig = tracerConfig;
      return this;
    }

    public NodeConfigBuilder setClusterPlugins(PluginInfo[] clusterPlugins) {
      this.clusterPlugins = clusterPlugins;
      return this;
    }

    public NodeConfigBuilder setDefaultZkHost(String defaultZkHost) {
      this.defaultZkHost = defaultZkHost;
      return this;
    }

    public NodeConfigBuilder setAllowPaths(Set<Path> paths) {
      this.allowPaths = paths;
      return this;
    }

    public NodeConfigBuilder setAllowUrls(List<String> urls) {
      this.allowUrls = urls;
      return this;
    }

    public NodeConfigBuilder setHideStackTrace(boolean hide) {
      this.hideStackTrace = hide;
      return this;
    }

    public NodeConfigBuilder setConfigSetServiceClass(String configSetServiceClass) {
      this.configSetServiceClass = configSetServiceClass;
      return this;
    }

    /**
     * Set list of modules to add to class path
     *
     * @param moduleNames comma separated list of module names to add to class loader, e.g.
     *     "extracting,ltr,langid"
     */
    public NodeConfigBuilder setModules(String moduleNames) {
      this.modules = moduleNames;
      return this;
    }

    public NodeConfigBuilder setHiddenSysProps(String hiddenSysProps) {
      this.hiddenSysProps = hiddenSysProps;
      return this;
    }

    /**
     * Finds list of hiddenSysProps requested in priority of solr.xml, system properties or the
     * default set
     *
     * @return set of raw hidden system properties, may be regex
     */
    private Set<String> resolveHiddenSysProps(String hiddenSysProps) {
      // Fall back to system properties if nothing configured through solr.xml
      if (!StrUtils.isNotNullOrEmpty(hiddenSysProps)) {
        String fromProps = EnvUtils.getProperty("solr.responses.hidden.sys.props");
        if (StrUtils.isNotNullOrEmpty(fromProps)) {
          hiddenSysProps = fromProps;
        }
      }
      Set<String> hiddenSysPropSet = Collections.emptySet();
      if (hiddenSysProps != null) {
        hiddenSysPropSet =
            StrUtils.splitSmart(hiddenSysProps, ',').stream()
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toSet());
      }
      return hiddenSysPropSet.isEmpty() ? DEFAULT_HIDDEN_SYS_PROPS : hiddenSysPropSet;
    }

    public NodeConfig build() {
      // if some things weren't set then set them now.  Simple primitives are set on the field
      // declaration
      if (loader == null) {
        loader = new SolrResourceLoader(solrHome);
      }
      return new NodeConfig(
          nodeName,
          coreRootDirectory,
          coresLocatorClass,
          coreSorterClass,
          solrDataHome,
          booleanQueryMaxClauseCount,
          configSetBaseDirectory,
          sharedLibDirectory,
          shardHandlerFactoryConfig,
          updateShardHandlerConfig,
          coreAdminHandlerClass,
          coreAdminHandlerActions,
          collectionsAdminHandlerClass,
          healthCheckHandlerClass,
          infoHandlerClass,
          configSetsHandlerClass,
          logWatcherConfig,
          cloudConfig,
          coreLoadThreads,
          replayUpdatesThreads,
          indexSearcherExecutorThreads,
          useSchemaCache,
          solrHome,
          loader,
          solrProperties,
          backupRepositoryPlugins,
          metricsConfig,
          cachesConfig,
          tracerConfig,
          clusterPlugins,
          defaultZkHost,
          allowPaths,
          allowUrls,
          hideStackTrace,
          configSetServiceClass,
          modules,
          resolveHiddenSysProps(hiddenSysProps));
    }

    public NodeConfigBuilder setSolrResourceLoader(SolrResourceLoader resourceLoader) {
      this.loader = resourceLoader;
      return this;
    }
  }
}
